iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 4
1

安裝所需套件

使用 pip 安裝

# 解析 HTML
pip install pyquery

解析 HTML

以取得加權指數成分股暨市值比重資料為例。

分析資料結構

https://www.taifex.com.tw/cht/9/futuresQADetail
加權指數成分股暨市值比重

  1. 表格有標頭行。
  2. 一行有二筆資料,且非依序排列,故資料取出後,視需求可能需要重新排序。
  3. 百分比僅紀錄到小數第 4 位,有重複資料的狀況,若要依照市值佔比排序,則無法只靠百分比欄位排序,仍需透過排行欄位進行市值佔比排序。

定義儲存類別

class Propotion:
    def __init__(self, sort, code, name, percent):
        # 排序
        self.Sort = sort
        # 證券代碼
        self.Code = code
        # 證券名稱
        self.Name = name
        # 市值佔比
        self.Percent = percent

下載資料並解析

使用 PyQuery 解析 HTML 內容,因 PyQuery 套件庫用法參考自 jQuery,故函數用法等均類同於 jQuery,對於多數開發者而言應較 Beautiful Soup 4 更加容易上手。

import datetime

import chardet
import loguru
import pyquery
import requests

def main():
    resp = requests.get('https://www.taifex.com.tw/cht/9/futuresQADetail')
    if resp.status_code != 200:
        loguru.logger.error('RESP: status code is not 200')
    loguru.logger.success('RESP: success')

    txt = None
    det = chardet.detect(resp.content)
    try:
        if det['confidence'] > 0.5:
            if det['encoding'] == 'big-5':
                txt = resp.content.decode('big5')
            else:
                txt = resp.content.decode(det['encoding'])
        else:
            txt = resp.content.decode('utf-8')
    except Exception as e:
        loguru.logger.error(e)

    if txt is None:
        return
    loguru.logger.info(txt)

    # 成分股市值佔比清單
    proportions = []

    # 將下載回來的內容解析為 PyQuery 物件
    d = pyquery.PyQuery(txt)
    # 透過 CSS 選擇器取出所有表格行
    trs = list(d('table tr').items())
    # 去除標頭行(分析結果 1.)
    trs = trs[1:]
    # 依序取出資料行
    for tr in trs:
        # 取出所有資料格
        tds = list(tr('td').items())
        #
        # 取出資料行中第一組證券內容(分析結果 2.)
        #
        # 取出證券代碼欄位值
        code = tds[1].text().strip()
        # 若證券代碼欄位值存在資料,代表本筆資料存在,則繼續取出其他欄位
        if code != '':
            # 取出排序欄位值
            sort = tds[1].text().strip()
            # 取出證券名稱欄位值
            name = tds[2].text().strip()
            # 取出市值佔比欄位值
            percent = tds[3].text().strip()
            # 將取得資料存入成分股市值佔比清單
            proportions.append(Propotion(
                sort=sort,
                code=code,
                name=name,
                percent=percent
            ))
        #
        # 取出資料行中第二組證券內容(分析結果 2.)
        #
        # 取出證券代碼欄位值
        code = tds[5].text().strip()
        # 若證券代碼欄位值存在資料,代表本筆資料存在,則繼續取出其他欄位
        if code != '':
            # 取出排序欄位值
            sort = tds[5].text().strip()
            # 取出證券名稱欄位值
            name = tds[6].text().strip()
            # 取出市值佔比欄位值
            percent = tds[7].text().strip()
            # 將取得資料存入成分股市值佔比清單
            proportions.append(Propotion(
                sort=sort,
                code=code,
                name=name,
                percent=percent
            ))

    # 按證券代碼順序重新排列資列並輸出(分析結果 3.)
    proportions.sort(key=lambda proportion: proportion.Code)
    loguru.logger.info(proportions)

if __name__ == '__main__':
    loguru.logger.add(
        f'{datetime.date.today():%Y%m%d}.log',
        rotation='1 day',
        retention='7 days',
        level='DEBUG'
    )
    main()

重新定義資料儲存結構

因為存入資料都是字串,但實際內容有整數(排序),還有浮點數(市值佔比),所以需要在儲存類別的建構子中進行轉換處理。

import fractions

class Propotion:
    # 建構子
    def __init__(self, sort, code, name, percent):
        # 排序
        # 轉換為整數,以利後續排序使用
        self.Sort = int(sort)
        # 證券代碼
        self.Code = code
        # 證券名稱
        self.Name = name
        # 市值佔比
        # 不建議使用 float 進行浮點數保存,後續可能會造成浮點數計算誤差,
        # 使用 Fraction 保存才能有效降低浮點數計算結果的誤差
        self.Percent = fractions.Fraction(percent[:-1])
    # 物件表達式
    def __repr__(self):
        return (
            f'class Propotion {{ '
            f'Sort={self.Sort}, '
            f'Code={self.Code}, '
            f'Name={self.Name}, '
            f'Percent={float(self.Percent):.4f}% '
            f'}}'
        )

重新調整輸出方式

在定義儲存物件的表達式後,我們會看到輸出如下圖:
執行結果
雖然已經格式化,但卻有點雜亂難以閱讀,所以必須修正輸出方式。

import datetime
import fractions
import os

import chardet
import loguru
import pyquery
import requests

class Propotion:
    def __init__(self, sort, code, name, percent):
        self.Sort = int(sort)
        self.Code = code
        self.Name = name
        self.Percent = fractions.Fraction(percent[:-1])
    def __repr__(self):
        return (
            f'class Propotion {{ '
            f'Sort={self.Sort}, '
            f'Code={self.Code}, '
            f'Name={self.Name}, '
            f'Percent={float(self.Percent):.4f}% '
            f'}}'
        )

def main():
    resp = requests.get('https://www.taifex.com.tw/cht/9/futuresQADetail')
    if resp.status_code != 200:
        loguru.logger.error('RESP: status code is not 200')
    loguru.logger.success('RESP: success')

    txt = None
    det = chardet.detect(resp.content)
    try:
        if det['confidence'] > 0.5:
            if det['encoding'] == 'big-5':
                txt = resp.content.decode('big5')
            else:
                txt = resp.content.decode(det['encoding'])
        else:
            txt = resp.content.decode('utf-8')
    except Exception as e:
        loguru.logger.error(e)

    if txt is None:
        return
    loguru.logger.info(txt)

    proportions = []

    d = pyquery.PyQuery(txt)
    trs = list(d('table tr').items())
    trs = trs[1:]
    for tr in trs:
        tds = list(tr('td').items())
        code = tds[1].text().strip()
        if code != '':
            sort = tds[1].text().strip()
            name = tds[2].text().strip()
            percent = tds[3].text().strip()
            proportions.append(Propotion(
                sort=sort,
                code=code,
                name=name,
                percent=percent
            ))
        code = tds[5].text().strip()
        if code != '':
            sort = tds[5].text().strip()
            name = tds[6].text().strip()
            percent = tds[7].text().strip()
            proportions.append(Propotion(
                sort=sort,
                code=code,
                name=name,
                percent=percent
            ))

    proportions.sort(key=lambda proportion: proportion.Code)
    loguru.logger.info(proportions)

    # 將每筆物件表達式輸出的字串以系統換行符號相接,讓每筆物件表達式各自獨立一行
    message = os.linesep.join([str(proportion) for proportion in proportions])
    loguru.logger.info('PROPORTIONS' + os.linesep + message)

if __name__ == '__main__':
    loguru.logger.add(
        f'{datetime.date.today():%Y%m%d}.log',
        rotation='1 day',
        retention='7 days',
        level='DEBUG'
    )
    main()

重新執行得到適合人類閱讀的輸出結果。
執行結果

Would You Like To Know More?

https://pythonhosted.org/pyquery/


團隊系列文:

CSScoke - 金魚都能懂的這個網頁畫面怎麼切 - 金魚都能懂了你還怕學不會嗎
Clarence - LINE bot 好好玩 30 天玩轉 LINE API
Hina Hina - 陣列大亂鬥
King Tzeng - IoT沒那麼難!新手用JavaScript入門做自己的玩具
Vita Ora - 好 Js 不學嗎 !? JavaScript 入門中的入門。
TaTaMo - 用Python開發的網頁不能放到Github上?Lektor說可以!!


上一篇
Day-03 資料蒐集:取得加權指數成分股暨市值比重資料
下一篇
Day-05 資料蒐集:取得代理清單
系列文
Python 程式交易 30 天新手入門30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言